理解设计模式之----命令模式
零零碎碎的了解过部分设计模式,但没有系统的学习过,最近晚上有点时间,就买了本程杰的《大话设计模式》,最近想系统的学习下。当看到命令模式的时候,感觉并不是太好理解,于是上网搜索了些资料。发现对设计模式的看法多少很多文章都有些不一样,于是想写下自己对命令模式的一些看法,以加深理解。要是文章有不对的地方,希望大家能提出改进建议。
目的:
任何模式的出现,都是为了解决一些特定的场景的耦合问题,以达到对修改封闭,对扩展开放的效果。命令模式也不例外:
命令模式是为了解决命令的请求者和命令的实现者之间的耦合关系。
解决了这种耦合的好处我认为主要有两点:
1.更方便的对命令进行扩展(注意:这不是主要的优势,后面会提到)
2.对多个命令的统一控制(这种控制包括但不限于:队列、撤销/恢复、记录日志等等)
模式解析:
经典的命令模式包括4个角色:
Command:定义命令的统一接口
ConcreteCommand:Command接口的实现者,用来执行具体的命令,某些情况下可以直接用来充当Receiver。
Receiver:命令的实际执行者
Invoker:命令的请求者,是命令模式中最重要的角色。这个角色用来对各个命令进行控制。
下面对上面四个角色的经典实现用代码来进行说明,这也是大部分文章对命令模式的运用方式。
经典代码实现:
1 /// <summary> 2 /// Command角色 3 /// </summary> 4 public interface ICommand 5 { 6 void Execute(); 7 } 8 9 /// <summary> 10 /// ConcreteCommand角色A 11 /// </summary> 12 public class ConcreteCommandA : ICommand 13 { 14 private Receiver receiver = null; 15 16 public ConcreteCommandA(Receiver receiver) 17 { 18 this.receiver = receiver; 19 } 20 21 public void Execute() 22 { 23 this.receiver.DoA(); 24 } 25 } 26 27 /// <summary> 28 /// ConcreteCommand角色B 29 /// </summary> 30 public class ConcreteCommandB : ICommand 31 { 32 private Receiver receiver = null; 33 34 public ConcreteCommandB(Receiver receiver) 35 { 36 this.receiver = receiver; 37 } 38 39 public void Execute() 40 { 41 this.receiver.DoB(); 42 } 43 } 44 45 /// <summary> 46 /// Receiver角色 47 /// </summary> 48 public class Receiver 49 { 50 public void DoA() 51 { 52 //DoSomething 53 } 54 55 public void DoB() 56 { 57 //DoSomething 58 } 59 } 60 61 /// <summary> 62 /// Invoker角色 63 /// </summary> 64 public class Invoker 65 { 66 private ICommand command = null; 67 68 //设置命令 69 public void SetCommand(ICommand command) 70 { 71 this.command = command; 72 } 73 //执行命令 74 public void RunCommand() 75 { 76 command.Execute(); 77 } 78 } 79 80 /// <summary> 81 /// 客户端调用 82 /// </summary> 83 public class Client 84 { 85 public Client() 86 { 87 Receiver receiver = new Receiver(); 88 Invoker invoker = new Invoker(); 89 invoker.SetCommand(new ConcreteCommandA(receiver)); 90 invoker.RunCommand(); 91 invoker.SetCommand(new ConcreteCommandB(receiver)); 92 invoker.RunCommand(); 93 } 94 }
经典实现解析:
不知道大家看过上面的代码之后是什么感觉,反正我看过上面的代码之后第一反应确实是越看越糊涂了,主要觉得有几点疑问:
1. 执行命令可以,但是为什么要用命令封装起来,这不是有点脱裤子放屁的感觉么?我完全可以这样写:
1 public class Client 2 { 3 public Client() 4 { 5 Receiver receiver = new Receiver(); 6 receiver.DoA(); 7 receiver.DoB(); 8 } 9 }
这样不是更加简单明了?两个类搞定。
2. 通过继承ICommand之后,增加命令怎么增加?比如增加一个命令,要改动3个地方:增加一个ICommand实现,修改Receiver类,修改Client。这好像没有对修改关闭啊?
更有甚者,有些地方采用下面这种方式写Invoker类之后,要修改四个地方!
1 /// <summary> 2 /// Invoker角色 3 /// </summary> 4 public class Invoker 5 { 6 private ICommand commandA = null; 7 8 //设置命令A 9 public void SetCommandA(ICommand commandA) 10 { 11 this.commandA = commandA; 12 } 13 //执行命令A 14 public void RunCommandA() 15 { 16 commandA.Execute(); 17 } 18 19 private ICommand commandB = null; 20 21 //设置命令B 22 public void SetCommandB(ICommand commandB) 23 { 24 this.commandB = commandB; 25 } 26 //执行命令B 27 public void RunCommandB() 28 { 29 commandB.Execute(); 30 } 31 }
天啦!这简直是个噩梦,完全没感觉到对修改关闭啊。难道是我对命令模式完全理解错误了吗?
3. ConcreteCommandA和ConcreteCommandB与Receiver类完全耦合了啊,要是有ConcreteCommandA与ConcreteCommandB要执行的命令在不同的Receiver中怎么办?
看到这里,像我这种对设计模式一知半解的小伙伴估计完全懵了,在这话情况下对自己的智商产生了严重的怀疑,或者设计模式错了???
但实际情况真的是这样吗?NONONONO,可以想到绝对不是,这可是奉为经典的设计模式啊,好吧,那我们来看看到底错在哪里:
1. 确实可以两个类来搞定。但我们要牢记命令模式的初衷:对命令请求者(Invoker)和命令实现者(Receiver)的解耦,方便对命令进行各种控制。
打个比方:现在我们要对ConcreteCommandA与ConcreteCommandB以及其他一系列命令进行日志记录,并且两个命令之间的操作间隔不能大于1秒。
这种情况下要直接用两个类就会有大量的业务逻辑要在客户端进行处理,当命令增加,对每个命令的控制增加时,就会在Client里面产生大量的变化点,这样耦合就出来了,但是采用命令模式之后,对着一系列的命令我们都可以进行控制,这就是对变化点的封装,实际Invoker代码如下:
1 public class Invoker 2 { 3 private ICommand lastCommand = null; 4 private DateTime lastDateTime = DateTime.Now; 5 6 public void RunCommand(ICommand command) 7 { 8 //记录操作日志 9 Console.WriteLine(command.GetType().Name); 10 //大于1秒,执行命令 11 if (lastCommand == null || (DateTime.Now - this.lastDateTime).TotalSeconds > 1) 12 { 13 lastCommand = command; 14 lastDateTime = DateTime.Now; 15 command.Execute(); 16 } 17 //小于1秒时不执行,并进行相应处理 18 Console.WriteLine("操作间隔过短!"); 19 } 20 }
2. 增加命令:采用命令模式的时候,我感觉最大的耦合点变化到了Receiver和ConcreteCommand之间,当然我们可以对Receiver进行抽象,采用接口或者抽象类来封装这个变化,但实际情况中我们会遇到多个命令来至于不同的Receiver,比如A,B两个命令来至于ReceiverAB,C命令来至于ReceiverC,这种情况下我们怎么应对命令的新增?对这种情况我的理解是命令模式并不能也不需要解决这个问题,因为命令模式的操作单元已经细化到了每一个具体的功能上面,当增加一个具体功能的时候是没有很好的办法对功能实现类进行修改关闭的(当然你可以把每个功能方法放到一个类中,但确实没必要,这个粒度已经很小了),实际上也没有必要的。
打个比方:一个界面有增加删除功能,在一个类Receiver里面实现了,现在要新增一个修改功能,有必要新增一个Reeciver类吗?我自己的答案是没有必要。
但是当业务是所有的功能都会同时修改时,我们就可以对这Receiver进行抽象,提取出IReceiver。
不同Receiver实现:
1 /// <summary> 2 /// ConcreteCommand角色A 3 /// </summary> 4 public class ConcreteCommandA : ICommand 5 { 6 private ReceiverAB receiver = null; 7 8 public ConcreteCommandA(ReceiverAB receiver) 9 { 10 this.receiver = receiver; 11 } 12 13 public void Execute() 14 { 15 this.receiver.DoA(); 16 } 17 } 18 19 /// <summary> 20 /// ConcreteCommand角色B 21 /// </summary> 22 public class ConcreteCommandB : ICommand 23 { 24 private ReceiverAB receiver = null; 25 26 public ConcreteCommandB(ReceiverAB receiver) 27 { 28 this.receiver = receiver; 29 } 30 31 public void Execute() 32 { 33 this.receiver.DoB(); 34 } 35 } 36 37 /// <summary> 38 /// ConcreteCommand角色C 39 /// </summary> 40 public class ConcreteCommandC : ICommand 41 { 42 private ReceiverC receiver = null; 43 44 public ConcreteCommandC(ReceiverC receiver) 45 { 46 this.receiver = receiver; 47 } 48 49 public void Execute() 50 { 51 this.receiver.DoC(); 52 } 53 } 54 55 /// <summary> 56 /// Receiver角色 57 /// </summary> 58 public class ReceiverAB 59 { 60 public void DoA() 61 { 62 //DoSomething 63 } 64 65 public void DoB() 66 { 67 //DoSomething 68 } 69 } 70 71 /// <summary> 72 /// Receiver角色 73 /// </summary> 74 public class ReceiverC 75 { 76 public void DoC() 77 { 78 //DoSomething 79 } 80 }
相同Receiver实现:
1 /// <summary> 2 /// Command角色 3 /// </summary> 4 public interface ICommand 5 { 6 void Execute(); 7 } 8 9 /// <summary> 10 /// ConcreteCommand角色A 11 /// </summary> 12 public class ConcreteCommandA : ICommand 13 { 14 private IReceiver receiver = null; 15 16 public ConcreteCommandA(IReceiver receiver) 17 { 18 this.receiver = receiver; 19 } 20 21 public void Execute() 22 { 23 this.receiver.DoA(); 24 } 25 } 26 27 /// <summary> 28 /// ConcreteCommand角色B 29 /// </summary> 30 public class ConcreteCommandB : ICommand 31 { 32 private IReceiver receiver = null; 33 34 public ConcreteCommandB(IReceiver receiver) 35 { 36 this.receiver = receiver; 37 } 38 39 public void Execute() 40 { 41 this.receiver.DoB(); 42 } 43 } 44 45 /// <summary> 46 /// ConcreteCommand角色B 47 /// </summary> 48 public class ConcreteCommandC : ICommand 49 { 50 private IReceiver receiver = null; 51 52 public ConcreteCommandC(IReceiver receiver) 53 { 54 this.receiver = receiver; 55 } 56 57 public void Execute() 58 { 59 this.receiver.DoA(); 60 } 61 } 62 63 public interface IReceiver 64 { 65 void DoA(); 66 67 void DoB(); 68 } 69 70 /// <summary> 71 /// Receiver角色 72 /// </summary> 73 public class ReceiverOne : IReceiver 74 { 75 public void DoA() 76 { 77 //DoSomething 78 } 79 80 public void DoB() 81 { 82 //DoSomething 83 } 84 } 85 86 /// <summary> 87 /// Receiver角色 88 /// </summary> 89 public class ReceiverTwo: IReceiver 90 { 91 public void DoA() 92 { 93 //DoSomething 94 } 95 96 public void DoB() 97 { 98 //DoSomething 99 } 100 }
3. 这个问题已经在上面的解答中一并说明了,这里不再做说明。
适用场景:
1. 命令的发送者和命令执行者有不同的生命周期。命令发送了并不是立即执行。
2. 命令需要进行各种管理逻辑。
3. 需要支持撤消\重做操作(这种状况的代码大家可以上网搜索下,有很多,这里不进行详细解读)。
结论:
通过对上面的分析我们可以知道如下几点:
1. 命令模式是通过命令发送者和命令执行者的解耦来完成对命令的具体控制的。
2. 命令模式是对功能方法的抽象,并不是对对象的抽象。
3. 命令模式是将功能提升到对象来操作,以便对多个功能进行一系列的处理以及封装。
好了,我对命令模式的理解到这里就结束了,如果大家发现有什么错误的地方,希望能不吝指正。如果有疑问的地方也可以提出,共同进步。
如果觉得文章对你有帮助,请点个赞喽,嘿嘿!